﻿// Tag.cs
//
// Copyright 2011 by Burt Harris
// Licensed under the Microsoft Public License (Ms-PL)
// See: http://www.codeplex.com/yaml/license

using System;
using System.Diagnostics.Contracts;

namespace YaTools.Yaml
{
    /// <summary>
    /// Tag = Uri + Kind
    /// </summary>
    [Serializable]
    public struct Tag
    {
        private const string Unallowed = "<>#\"{}|\\^[]`";

        // Tags use interned strings for speed of comparison
        private readonly string _uri;

        public const string LocalHandle = "!";
        public const string GlobalHandle = "!!";
        public const string DefaultGlobalPrefix = "tag:yaml.org,2002:";

        public static readonly Tag String, Sequence, Map, Null, Boolean, Integer;
        public static readonly Tag Float, OrderedMap, Pairs, Set, Binary, Merge;
        public static readonly Tag Value, Timestamp, Yaml, NonSpecific, NonSpecificNotPlainScalar;
        public static readonly Tag TaggedScalar, TaggedMap, TaggedSeq;

        private Tag(string special) // Special case use only
        {
            Contract.Requires(!string.IsNullOrEmpty(special));
            _uri = string.Intern(special);
        }

        public Tag(string name, Kind kind)
        {
                   // local tag starts with ! and is not URI
            name = name.StartsWith(LocalHandle) ? name : new Uri(name, UriKind.Absolute).AbsoluteUri.Normalize();

            switch (kind)
            {
                case Kind.Scalar:
                    _uri = string.Intern("sca:" + name);
                    break;
                case Kind.Mapping:
                    _uri = string.Intern("map:" + name);
                    break;
                case Kind.Sequence:
                    _uri = string.Intern("seq:" + name);
                    break;
                default:
                    throw new ArgumentException("Unexpected Kind", "kind");
            }
        }

        public string Name
        {
            get { return _uri.Substring(4); }
        }

        public Kind Kind
        {
            get
            {
                switch (_uri[1])
                {
                    case 'c':
                        return Kind.Scalar;

                    case 'a':
                        return Kind.Mapping;

                    case 'e':
                        return Kind.Sequence;
                }
                throw new InvalidOperationException("Unexpect tag: " + _uri);
            }
        }

        public override bool Equals(object obj)
        {
            return (obj is Tag) ? object.ReferenceEquals(_uri, ((Tag) obj)._uri) : false;
        }

        public override int GetHashCode()
        {
            return _uri.GetHashCode();
        }

        public override string ToString()
        {
            return _uri;
        }

        static Tag()
        {
            String = new Tag("tag:yaml.org,2002:str", Kind.Scalar);
            Sequence = new Tag("tag:yaml.org,2002:seq", Kind.Sequence);
            Map = new Tag("tag:yaml.org,2002:map", Kind.Mapping);
            Null = new Tag("tag:yaml.org,2002:null", Kind.Scalar);
            Boolean = new Tag("tag:yaml.org,2002:bool", Kind.Scalar);
            Integer = new Tag("tag:yaml.org,2002:int", Kind.Scalar);
            Float = new Tag("tag:yaml.org,2002:float", Kind.Scalar);
            OrderedMap = new Tag("tag:yaml.org,2002:omap", Kind.Sequence);
            Pairs = new Tag("tag:yaml.org,2002:pairs", Kind.Sequence);
            Set = new Tag("tag:yaml.org,2002:set", Kind.Mapping);
            Binary = new Tag("tag:yaml.org,2002:binary", Kind.Scalar);
            Merge = new Tag("tag:yaml.org,2002:merge", Kind.Scalar);
            Value = new Tag("tag:yaml.org,2002:value", Kind.Scalar);
            Timestamp = new Tag("tag:yaml.org,2002:timestamp", Kind.Scalar);
            Yaml = new Tag("tag:yaml.org,2002:yaml", Kind.Scalar);
            NonSpecific = new Tag(" ?  "); // Speical case, not a valid URI
            NonSpecificNotPlainScalar = new Tag(" !  "); // Speical case, not a valid URI
            TaggedMap = new Tag("tag:yatools.com,2009:tagged:map", Kind.Mapping);
            TaggedSeq = new Tag("tag:yatools.com,2009:tagged:seq", Kind.Sequence);
            TaggedScalar = new Tag("tag:yatools.com,2009:tagged:scalar", Kind.Scalar);
        }

        public static bool operator ==(Tag a, Tag b)
        {
            return object.ReferenceEquals(a._uri, b._uri);
        }

        public static bool operator !=(Tag a, Tag b)
        {
            return !object.ReferenceEquals(a._uri, b._uri);
        }

        public static bool IsValidTagPrefix(string prefix)
        {
            Contract.Requires(!string.IsNullOrEmpty(prefix));

            int fragmentIndex = prefix.IndexOf('#');
            if (fragmentIndex > 1)
            {
                string fragmentID = prefix.Substring(fragmentIndex + 1);
                prefix = prefix.Substring(0, fragmentIndex);

                // Validate the fragment (seems like the URI class 
                // should be able to do this...)

                for (int i = 0; i < fragmentID.Length; i++)
                {
                    char c = fragmentID[i];
                    if (c <= ' ' || c > '~') return false;
                    if (Unallowed.IndexOf(c) >= 0) return false;
                    if (c == '%')
                    {
                        if (i + 2 >= fragmentID.Length) return false;
                        if (!Uri.IsHexDigit(fragmentID[++i])
                            || !Uri.IsHexDigit(fragmentID[++i])) return false;
                    }
                }
            }

            return Uri.IsWellFormedUriString(prefix,
                                             IsLocalTag(prefix) ? UriKind.Relative : UriKind.Absolute);
        }

        public static bool IsLocalTag(string tag)
        {
            // CONSIDER: Should we check for a 2nd ! (making it other than a local tag)

            Contract.Requires(!string.IsNullOrEmpty(tag));
            return tag[0] == '!';
        }
    }
}
